package fi.iki.juri.units;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

/**
 * Creates Converters between requested units.
 * 
 * @author Juri Pakaste <juri@iki.fi>
 */
public class ConverterFactory {
	private final Map<String, Unit> converters;
	private final Map<String, ? extends Map<String, Unit>> translations;
	private final Calculation emptyCalculation = new Calculation() {
		public double perform(double x) {
			return x;
		}
		
		public String toString() {
			return "(Empty calculation)";
		}
	};
	
	/**
	 * Creates a new ConverterFactory with the converters and translations acquired from a generated units class.
	 * 
	 * @param converters
	 * @param translations
	 */
	public ConverterFactory(final Map<String, Unit> converters, final Map<String, ? extends Map<String, Unit>> translations) {
		this.converters = converters;
		this.translations = translations;
	}
	
	/**
	 * Recursively finds a Converter.
	 * 
	 * @param converter A converter created thus far in the recursion, should initially contain the Unit to start from 
	 * @param goal The Unit to look for
	 * @return The desired Converter, or if not found, the one passed in as the original argument
	 */
	private Converter findConverter(final Converter converter, final Unit goal) {
		for (Conversion edge : converter.getHead().getUnit().getEdges()) {
			if (edge.getUnit().equals(goal)) {
				return new Converter(edge, converter);
			} else if (! converter.hasConverter(edge.getUnit())) {
				final Converter result = findConverter(new Converter(edge, converter), goal);
				if (! result.getHead().equals(edge)) {
					return result;
				}
			}
		}
		return converter;
	}
	
	/**
	 * Returns a converter for the desired units.
	 * 
	 * @param from The id or alias of the Unit to convert from
	 * @param to The id or alias of the Unit to convert to
	 * @return A Converter for the Units, or null if not found 
	 * @throws ConverterException if one of the desired units isn't found
	 */
	public Converter getConverter(final String from, final String to) throws ConverterException {
		final Unit start = converters.get(from);
		final Unit goal = converters.get(to);
		if (start == null)
			throw new ConverterException("No converter found for the from unit: " + from);
		if (goal == null)
			throw new ConverterException("No converter found for the to unit: " + to);
		final Converter startPipe = new Converter(new Conversion(start, emptyCalculation), null);
		if (start.equals(goal)) {
			return startPipe;
		}
		final Converter solution = findConverter(startPipe, goal);
		return startPipe == solution ? null : solution;
	}

	/**
	 * Returns a converter for the desired units. Tries first in the specified locale, after that with the default names.
	 * 
	 * @param from The id, alias or translated name of the Unit to convert from
	 * @param to The id, alias or translated name of the Unit to convert to
	 * @param locale The locale to look in for the translated name
	 * @return A Converter for the Units, or null if not found
	 * @throws LocaleException if the specified locale isn't found
	 * @throws ConverterException if one of the desired units isn't found
	 */
	public Converter getConverter(final String from, final String to, final String locale) 
			throws LocaleException, ConverterException {
		if (! translations.containsKey(locale))
			throw new LocaleException("Unknown locale: " + locale);
		final Map<String,Unit> translation = translations.get(locale);
		final Unit start = translation.containsKey(from) ?
				translation.get(from) : converters.get(from);
		final Unit goal = translation.containsKey(to) ?
				translation.get(to) : converters.get(to);
		if (start == null)
			throw new ConverterException("No converter found for the from unit: " + from);
		if (goal == null)
			throw new ConverterException("No converter found for the to unit: " + to);
		final Converter startPipe = new Converter(new Conversion(start, emptyCalculation), null);
		final Converter solution = findConverter(startPipe, goal);
		return startPipe == solution ? null : solution;
	}
	
	/**
	 * Returns the known locales as an unmodifiable Set.
	 * 
	 * @return
	 */
	public Set<String> getKnownLocales() {
		return Collections.unmodifiableSet(translations.keySet());
	}
	
	/**
	 * Returns true if the specified locale is known.
	 * 
	 * @param locale
	 * @return
	 */
	public boolean hasLocale(final String locale) {
		return translations.containsKey(locale);
	}
	
	/**
	 * Returns the known unit names and aliases as an unmodifiable Set.
	 * 
	 * @return
	 */
	public Set<String> getUnitNames() {
		return Collections.unmodifiableSet(converters.keySet());
	}

	/**
	 * Returns the translated unit names in the specified locale as an unmodifiable Set.
	 * 
	 * @param locale the locale to look for the unit name in
	 * @return
	 * @throws LocaleException if the specified locale isn't known
	 */
	public Set<String> getTranslatedUnitNames(final String locale)
			throws LocaleException {
		if (! translations.containsKey(locale))
			throw new LocaleException("Unknown locale: " + locale);
		return Collections.unmodifiableSet(translations.get(locale).keySet());
	}

	/**
	 * Return the named Unit, or null if it doesn't exist.
	 * 
	 * @return the Unit with the specified name, or null if it doesn't exist.
	 */
	public Unit getUnit(final String name) {
		return converters.get(name);
	}
}
